// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/browser/notifications/platform_notification_context_impl.h"

#include "base/bind_helpers.h"
#include "base/files/file_util.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_macros.h"
#include "base/threading/sequenced_worker_pool.h"
#include "content/browser/notifications/blink_notification_service_impl.h"
#include "content/browser/notifications/notification_database.h"
#include "content/browser/service_worker/service_worker_context_wrapper.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/notification_database_data.h"
#include "content/public/browser/platform_notification_service.h"

using base::DoNothing;

namespace content {
namespace {

    // Name of the directory in the user's profile directory where the notification
    // database files should be stored.
    const base::FilePath::CharType kPlatformNotificationsDirectory[] = FILE_PATH_LITERAL("Platform Notifications");

} // namespace

PlatformNotificationContextImpl::PlatformNotificationContextImpl(
    const base::FilePath& path,
    BrowserContext* browser_context,
    const scoped_refptr<ServiceWorkerContextWrapper>& service_worker_context)
    : path_(path)
    , browser_context_(browser_context)
    , service_worker_context_(service_worker_context)
    , notification_id_generator_(browser_context)
{
    DCHECK_CURRENTLY_ON(BrowserThread::UI);
}

PlatformNotificationContextImpl::~PlatformNotificationContextImpl()
{
    DCHECK_CURRENTLY_ON(BrowserThread::UI);

    // If the database has been initialized, it must be deleted on the task runner
    // thread as closing it may cause file I/O.
    if (database_) {
        DCHECK(task_runner_);
        task_runner_->DeleteSoon(FROM_HERE, database_.release());
    }
}

void PlatformNotificationContextImpl::Initialize()
{
    DCHECK_CURRENTLY_ON(BrowserThread::UI);
    PlatformNotificationService* service = GetContentClient()->browser()->GetPlatformNotificationService();
    if (service) {
        std::set<std::string> displayed_notifications;

        bool notification_synchronization_supported = service->GetDisplayedNotifications(browser_context_,
            &displayed_notifications);

        // Synchronize the notifications stored in the database with the set of
        // displaying notifications in |displayed_notifications|. This is necessary
        // because flakiness may cause a platform to inform Chrome of a notification
        // that has since been closed, or because the platform does not support
        // notifications that exceed the lifetime of the browser process.

        // TODO(peter): Synchronizing the actual notifications will be done when the
        // persistent notification ids are stable. For M44 we need to support the
        // case where there may be no notifications after a Chrome restart.
        if (notification_synchronization_supported && displayed_notifications.empty()) {
            prune_database_on_open_ = true;
        }
    }

    BrowserThread::PostTask(
        BrowserThread::IO, FROM_HERE,
        base::Bind(&PlatformNotificationContextImpl::InitializeOnIO, this));
}

void PlatformNotificationContextImpl::InitializeOnIO()
{
    DCHECK_CURRENTLY_ON(BrowserThread::IO);

    // |service_worker_context_| may be NULL in tests.
    if (service_worker_context_)
        service_worker_context_->AddObserver(this);
}

void PlatformNotificationContextImpl::Shutdown()
{
    DCHECK_CURRENTLY_ON(BrowserThread::UI);
    BrowserThread::PostTask(
        BrowserThread::IO, FROM_HERE,
        base::Bind(&PlatformNotificationContextImpl::ShutdownOnIO, this));
}

void PlatformNotificationContextImpl::ShutdownOnIO()
{
    DCHECK_CURRENTLY_ON(BrowserThread::IO);

    services_.clear();

    // |service_worker_context_| may be NULL in tests.
    if (service_worker_context_)
        service_worker_context_->RemoveObserver(this);
}

void PlatformNotificationContextImpl::CreateService(
    int render_process_id,
    mojo::InterfaceRequest<blink::mojom::NotificationService> request)
{
    DCHECK_CURRENTLY_ON(BrowserThread::UI);
    BrowserThread::PostTask(
        BrowserThread::IO, FROM_HERE,
        base::Bind(&PlatformNotificationContextImpl::CreateServiceOnIO, this,
            render_process_id, browser_context_->GetResourceContext(),
            base::Passed(&request)));
}

void PlatformNotificationContextImpl::CreateServiceOnIO(
    int render_process_id,
    ResourceContext* resource_context,
    mojo::InterfaceRequest<blink::mojom::NotificationService> request)
{
    DCHECK_CURRENTLY_ON(BrowserThread::IO);
    services_.push_back(base::MakeUnique<BlinkNotificationServiceImpl>(
        this, resource_context, render_process_id, std::move(request)));
}

void PlatformNotificationContextImpl::RemoveService(
    BlinkNotificationServiceImpl* service)
{
    DCHECK_CURRENTLY_ON(BrowserThread::IO);
    auto services_to_remove = std::remove_if(
        services_.begin(), services_.end(),
        [service](const std::unique_ptr<BlinkNotificationServiceImpl>& ptr) {
            return ptr.get() == service;
        });

    services_.erase(services_to_remove, services_.end());
}

void PlatformNotificationContextImpl::ReadNotificationData(
    const std::string& notification_id,
    const GURL& origin,
    const ReadResultCallback& callback)
{
    DCHECK_CURRENTLY_ON(BrowserThread::IO);

    LazyInitialize(
        base::Bind(&PlatformNotificationContextImpl::DoReadNotificationData, this,
            notification_id, origin, callback),
        base::Bind(callback, false /* success */, NotificationDatabaseData()));
}

void PlatformNotificationContextImpl::DoReadNotificationData(
    const std::string& notification_id,
    const GURL& origin,
    const ReadResultCallback& callback)
{
    DCHECK(task_runner_->RunsTasksOnCurrentThread());

    NotificationDatabaseData database_data;
    NotificationDatabase::Status status = database_->ReadNotificationData(notification_id, origin, &database_data);

    UMA_HISTOGRAM_ENUMERATION("Notifications.Database.ReadResult", status,
        NotificationDatabase::STATUS_COUNT);

    if (status == NotificationDatabase::STATUS_OK) {
        BrowserThread::PostTask(
            BrowserThread::IO, FROM_HERE,
            base::Bind(callback, true /* success */, database_data));
        return;
    }

    // Blow away the database if reading data failed due to corruption.
    if (status == NotificationDatabase::STATUS_ERROR_CORRUPTED)
        DestroyDatabase();

    BrowserThread::PostTask(
        BrowserThread::IO, FROM_HERE,
        base::Bind(callback, false /* success */, NotificationDatabaseData()));
}

void PlatformNotificationContextImpl::
    SynchronizeDisplayedNotificationsForServiceWorkerRegistration(
        const GURL& origin,
        int64_t service_worker_registration_id,
        const ReadAllResultCallback& callback,
        std::unique_ptr<std::set<std::string>> notification_ids,
        bool sync_supported)
{
    DCHECK_CURRENTLY_ON(BrowserThread::IO);
    LazyInitialize(
        base::Bind(&PlatformNotificationContextImpl::
                       DoReadAllNotificationDataForServiceWorkerRegistration,
            this, origin, service_worker_registration_id, callback,
            base::Passed(&notification_ids), sync_supported),
        base::Bind(callback, false /* success */,
            std::vector<NotificationDatabaseData>()));
}

void PlatformNotificationContextImpl::
    ReadAllNotificationDataForServiceWorkerRegistration(
        const GURL& origin,
        int64_t service_worker_registration_id,
        const ReadAllResultCallback& callback)
{
    DCHECK_CURRENTLY_ON(BrowserThread::IO);

    std::unique_ptr<std::set<std::string>> notification_ids = base::MakeUnique<std::set<std::string>>();

    PlatformNotificationService* service = GetContentClient()->browser()->GetPlatformNotificationService();

    if (!service) {
        // Rely on the database only
        SynchronizeDisplayedNotificationsForServiceWorkerRegistration(
            origin, service_worker_registration_id, callback,
            std::move(notification_ids), false /* sync_supported */);
        return;
    }

    std::set<std::string>* notification_ids_ptr = notification_ids.get();

    BrowserThread::PostTaskAndReplyWithResult(
        BrowserThread::UI, FROM_HERE,
        base::Bind(&PlatformNotificationService::GetDisplayedNotifications,
            base::Unretained(service), browser_context_,
            notification_ids_ptr),
        base::Bind(
            &PlatformNotificationContextImpl::
                SynchronizeDisplayedNotificationsForServiceWorkerRegistration,
            this, origin, service_worker_registration_id, callback,
            base::Passed(&notification_ids)));
}

void PlatformNotificationContextImpl::
    DoReadAllNotificationDataForServiceWorkerRegistration(
        const GURL& origin,
        int64_t service_worker_registration_id,
        const ReadAllResultCallback& callback,
        std::unique_ptr<std::set<std::string>> displayed_notifications,
        bool synchronization_supported)
{
    DCHECK(task_runner_->RunsTasksOnCurrentThread());
    DCHECK(displayed_notifications);

    std::vector<NotificationDatabaseData> notification_datas;

    NotificationDatabase::Status status = database_->ReadAllNotificationDataForServiceWorkerRegistration(
        origin, service_worker_registration_id, &notification_datas);

    UMA_HISTOGRAM_ENUMERATION("Notifications.Database.ReadForServiceWorkerResult",
        status, NotificationDatabase::STATUS_COUNT);

    if (status == NotificationDatabase::STATUS_OK) {
        if (synchronization_supported) {
            // Filter out notifications that are not actually on display anymore.
            // TODO(miguelg) synchronize the database if there are inconsistencies.
            for (auto it = notification_datas.begin();
                 it != notification_datas.end();) {
                // The database is only used for persistent notifications.
                DCHECK(NotificationIdGenerator::IsPersistentNotification(
                    it->notification_id));
                if (displayed_notifications->count(it->notification_id)) {
                    ++it;
                } else {
                    it = notification_datas.erase(it);
                }
            }
        }

        BrowserThread::PostTask(
            BrowserThread::IO, FROM_HERE,
            base::Bind(callback, true /* success */, notification_datas));
        return;
    }

    // Blow away the database if reading data failed due to corruption.
    if (status == NotificationDatabase::STATUS_ERROR_CORRUPTED)
        DestroyDatabase();

    BrowserThread::PostTask(BrowserThread::IO, FROM_HERE,
        base::Bind(callback, false /* success */,
            std::vector<NotificationDatabaseData>()));
}

void PlatformNotificationContextImpl::WriteNotificationData(
    const GURL& origin,
    const NotificationDatabaseData& database_data,
    const WriteResultCallback& callback)
{
    DCHECK_CURRENTLY_ON(BrowserThread::IO);
    LazyInitialize(
        base::Bind(&PlatformNotificationContextImpl::DoWriteNotificationData,
            this, origin, database_data, callback),
        base::Bind(callback, false /* success */, "" /* notification_id */));
}

void PlatformNotificationContextImpl::DoWriteNotificationData(
    const GURL& origin,
    const NotificationDatabaseData& database_data,
    const WriteResultCallback& callback)
{
    DCHECK(task_runner_->RunsTasksOnCurrentThread());
    DCHECK(database_data.notification_id.empty());

    // Eagerly delete data for replaced notifications from the database.
    if (!database_data.notification_data.tag.empty()) {
        std::set<std::string> deleted_notification_ids;
        NotificationDatabase::Status delete_status = database_->DeleteAllNotificationDataForOrigin(
            origin, database_data.notification_data.tag,
            &deleted_notification_ids);

        UMA_HISTOGRAM_ENUMERATION("Notifications.Database.DeleteBeforeWriteResult",
            delete_status,
            NotificationDatabase::STATUS_COUNT);

        // Unless the database was corrupted following this change, there is no
        // reason to bail out here in event of failure because the notification
        // display logic will handle notification replacement for the user.
        if (delete_status == NotificationDatabase::STATUS_ERROR_CORRUPTED) {
            DestroyDatabase();

            BrowserThread::PostTask(
                BrowserThread::IO, FROM_HERE,
                base::Bind(callback, false /* success */, "" /* notification_id */));
            return;
        }
    }

    // Create a copy of the |database_data| to store a generated notification ID.
    NotificationDatabaseData write_database_data = database_data;
    write_database_data.notification_id = notification_id_generator_.GenerateForPersistentNotification(
        origin, database_data.notification_data.tag,
        database_->GetNextPersistentNotificationId());

    NotificationDatabase::Status status = database_->WriteNotificationData(origin, write_database_data);

    UMA_HISTOGRAM_ENUMERATION("Notifications.Database.WriteResult", status,
        NotificationDatabase::STATUS_COUNT);

    if (status == NotificationDatabase::STATUS_OK) {
        BrowserThread::PostTask(BrowserThread::IO, FROM_HERE,
            base::Bind(callback, true /* success */,
                write_database_data.notification_id));

        return;
    }

    // Blow away the database if writing data failed due to corruption.
    if (status == NotificationDatabase::STATUS_ERROR_CORRUPTED)
        DestroyDatabase();

    BrowserThread::PostTask(
        BrowserThread::IO, FROM_HERE,
        base::Bind(callback, false /* success */, "" /* notification_id */));
}

void PlatformNotificationContextImpl::DeleteNotificationData(
    const std::string& notification_id,
    const GURL& origin,
    const DeleteResultCallback& callback)
{
    DCHECK_CURRENTLY_ON(BrowserThread::IO);

    LazyInitialize(
        base::Bind(&PlatformNotificationContextImpl::DoDeleteNotificationData,
            this, notification_id, origin, callback),
        base::Bind(callback, false /* success */));
}

void PlatformNotificationContextImpl::DoDeleteNotificationData(
    const std::string& notification_id,
    const GURL& origin,
    const DeleteResultCallback& callback)
{
    DCHECK(task_runner_->RunsTasksOnCurrentThread());

    NotificationDatabase::Status status = database_->DeleteNotificationData(notification_id, origin);

    UMA_HISTOGRAM_ENUMERATION("Notifications.Database.DeleteResult", status,
        NotificationDatabase::STATUS_COUNT);

    bool success = status == NotificationDatabase::STATUS_OK;

    // Blow away the database if deleting data failed due to corruption. Following
    // the contract of the delete methods, consider this to be a success as the
    // caller's goal has been achieved: the data is gone.
    if (status == NotificationDatabase::STATUS_ERROR_CORRUPTED) {
        DestroyDatabase();
        success = true;
    }

    BrowserThread::PostTask(BrowserThread::IO, FROM_HERE,
        base::Bind(callback, success));
}

void PlatformNotificationContextImpl::OnRegistrationDeleted(
    int64_t registration_id,
    const GURL& pattern)
{
    DCHECK_CURRENTLY_ON(BrowserThread::IO);
    LazyInitialize(
        base::Bind(&PlatformNotificationContextImpl::
                       DoDeleteNotificationsForServiceWorkerRegistration,
            this, pattern.GetOrigin(), registration_id),
        base::Bind(&DoNothing));
}

void PlatformNotificationContextImpl::
    DoDeleteNotificationsForServiceWorkerRegistration(
        const GURL& origin,
        int64_t service_worker_registration_id)
{
    DCHECK(task_runner_->RunsTasksOnCurrentThread());

    std::set<std::string> deleted_notification_ids;
    NotificationDatabase::Status status = database_->DeleteAllNotificationDataForServiceWorkerRegistration(
        origin, service_worker_registration_id, &deleted_notification_ids);

    UMA_HISTOGRAM_ENUMERATION(
        "Notifications.Database.DeleteServiceWorkerRegistrationResult", status,
        NotificationDatabase::STATUS_COUNT);

    // Blow away the database if a corruption error occurred during the deletion.
    if (status == NotificationDatabase::STATUS_ERROR_CORRUPTED)
        DestroyDatabase();

    // TODO(peter): Close the notifications in |deleted_notification_ids|. See
    // https://crbug.com/532436.
}

void PlatformNotificationContextImpl::OnStorageWiped()
{
    DCHECK_CURRENTLY_ON(BrowserThread::IO);
    LazyInitialize(
        base::Bind(
            base::IgnoreResult(&PlatformNotificationContextImpl::DestroyDatabase),
            this),
        base::Bind(&DoNothing));
}

void PlatformNotificationContextImpl::LazyInitialize(
    const base::Closure& success_closure,
    const base::Closure& failure_closure)
{
    DCHECK_CURRENTLY_ON(BrowserThread::IO);

    if (!task_runner_) {
        base::SequencedWorkerPool* pool = BrowserThread::GetBlockingPool();
        base::SequencedWorkerPool::SequenceToken token = pool->GetSequenceToken();

        task_runner_ = pool->GetSequencedTaskRunner(token);
    }

    task_runner_->PostTask(
        FROM_HERE, base::Bind(&PlatformNotificationContextImpl::OpenDatabase, this, success_closure, failure_closure));
}

void PlatformNotificationContextImpl::OpenDatabase(
    const base::Closure& success_closure,
    const base::Closure& failure_closure)
{
    DCHECK(task_runner_->RunsTasksOnCurrentThread());

    if (database_) {
        success_closure.Run();
        return;
    }

    database_.reset(new NotificationDatabase(GetDatabasePath()));
    NotificationDatabase::Status status = database_->Open(true /* create_if_missing */);

    UMA_HISTOGRAM_ENUMERATION("Notifications.Database.OpenResult", status,
        NotificationDatabase::STATUS_COUNT);

    // TODO(peter): Do finer-grained synchronization here.
    if (prune_database_on_open_) {
        prune_database_on_open_ = false;
        DestroyDatabase();

        database_.reset(new NotificationDatabase(GetDatabasePath()));
        status = database_->Open(true /* create_if_missing */);

        // TODO(peter): Find the appropriate UMA to cover in regards to
        // synchronizing notifications after the implementation is complete.
    }

    // When the database could not be opened due to corruption, destroy it, blow
    // away the contents of the directory and try re-opening the database.
    if (status == NotificationDatabase::STATUS_ERROR_CORRUPTED) {
        if (DestroyDatabase()) {
            database_.reset(new NotificationDatabase(GetDatabasePath()));
            status = database_->Open(true /* create_if_missing */);

            UMA_HISTOGRAM_ENUMERATION(
                "Notifications.Database.OpenAfterCorruptionResult", status,
                NotificationDatabase::STATUS_COUNT);
        }
    }

    if (status == NotificationDatabase::STATUS_OK) {
        success_closure.Run();
        return;
    }

    database_.reset();

    BrowserThread::PostTask(BrowserThread::IO, FROM_HERE, failure_closure);
}

bool PlatformNotificationContextImpl::DestroyDatabase()
{
    DCHECK(task_runner_->RunsTasksOnCurrentThread());
    DCHECK(database_);

    NotificationDatabase::Status status = database_->Destroy();
    UMA_HISTOGRAM_ENUMERATION("Notifications.Database.DestroyResult", status,
        NotificationDatabase::STATUS_COUNT);

    database_.reset();

    // TODO(peter): Close any existing persistent notifications on the platform.

    // Remove all files in the directory that the database was previously located
    // in, to make sure that any left-over files are gone as well.
    base::FilePath database_path = GetDatabasePath();
    if (!database_path.empty())
        return base::DeleteFile(database_path, true);

    return true;
}

base::FilePath PlatformNotificationContextImpl::GetDatabasePath() const
{
    if (path_.empty())
        return path_;

    return path_.Append(kPlatformNotificationsDirectory);
}

void PlatformNotificationContextImpl::SetTaskRunnerForTesting(
    const scoped_refptr<base::SequencedTaskRunner>& task_runner)
{
    task_runner_ = task_runner;
}

} // namespace content
